/*
 * Copyright (C) 2021 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package net.qiujuer.genius.ui.element;

import ohos.agp.components.element.ShapeElement;
import ohos.agp.render.Canvas;
import ohos.agp.render.ColorFilter;
import ohos.agp.render.Paint;
import ohos.agp.render.Path;
import ohos.agp.utils.Color;
import ohos.agp.utils.Rect;
import ohos.eventhandler.EventHandler;

import java.lang.ref.WeakReference;
import java.util.Random;

/**
 * RipElement
 *
 * @since 2021-05-10
 */
public class RipElement extends ShapeElement {
    private Paint mPaint = new Paint();
    private Path mPath = new Path();
    private Random mRandom = new Random();
    private int mMaxDeepness = 24;
    private int mMinDeepness = 4;
    private float mFactor = 1.2f;
    private Rect mFluCount = new Rect();
    private int mColor;
    private boolean mSmooth = true;
    private WeakReference<RipCallback> mCallback = null;

    /**
     * constructor
     */
    public RipElement() {
        mPaint.setAntiAlias(true);
        mPaint.setStyle(Paint.Style.FILL_STYLE);
        mPaint.setDither(true);
        mFluCount.top = 36;
        mFluCount.left = 25;
        mFluCount.right = 25;
        mFluCount.bottom = 36;
    }

    /**
     * set flu count
     *
     * @param left
     * @param top
     * @param right
     * @param bottom
     */
    public void setFluCount(int left, int top, int right, int bottom) {
        mFluCount.set(left, top, right, bottom);
    }

    /**
     * set deepness
     *
     * @param min
     * @param max
     */
    public void setDeepness(int min, int max) {
        mMinDeepness = min;
        mMaxDeepness = max;
    }

    /**
     * enable random
     *
     * @param random
     */
    public void setIsRandom(boolean random) {
        if (random) {
            if (mRandom == null) {
                mRandom = new Random();
            }
        } else {
            mRandom = null;
        }
    }

    /**
     * get color value
     *
     * @return color
     */
    public int getColor() {
        return mColor;
    }

    /**
     * set color for paint
     *
     * @param color
     */
    public void setColor(int color) {
        if (mColor == color) {
            return;
        }
        mColor = color;
        mPaint.setColor(new Color(color));
        invalidateSelf();
    }

    /**
     * set color
     *
     * @param color
     */
    public void setColorUnInvalidate(int color) {
        if (mColor == color) {
            return;
        }
        mColor = color;
        mPaint.setColor(new Color(color));
    }

    public Paint getPaint() {
        return mPaint;
    }

    /**
     * set smooth
     *
     * @param smooth
     */
    public void setSmooth(boolean smooth) {
        if (this.mSmooth != smooth) {
            this.mSmooth = smooth;
            initPath(getBounds());
        }
    }

    public boolean isSmooth() {
        return mSmooth;
    }

    /**
     * set random true or false
     *
     * @param isRandom
     */
    public void setRandom(boolean isRandom) {
        if (isRandom) {
            if (mRandom == null) {
                mRandom = new Random();
            }
        } else {
            mRandom = null;
        }
    }

    /**
     * return random
     *
     * @return true or false
     */
    public boolean isRandom() {
        return mRandom != null;
    }

    /**
     * draw
     *
     * @param canvas
     */
    public void draw(Canvas canvas) {
        draw(canvas, mPath, mPaint);
    }

    /**
     * draw
     *
     * @param canvas
     * @param path
     * @param paint
     */
    protected void draw(Canvas canvas, Path path, Paint paint) {
        canvas.drawPath(path, paint);
    }

    /**
     * get paint alpha
     *
     * @return alpha
     */
    @Override
    public int getAlpha() {
        return (int) mPaint.getAlpha();
    }

    /**
     * set alpha for paint
     *
     * @param alpha
     */
    @Override
    public void setAlpha(int alpha) {
        mPaint.setAlpha(alpha);
    }

    /**
     * set color filter for paint
     *
     * @param cf
     */
    public void setColorFilter(ColorFilter cf) {
        mPaint.setColorFilter(cf);
    }

    /**
     * get opacity
     *
     * @return opacity
     */
    public int getOpacity() {
        return 0;
    }

    /**
     * bounds change
     *
     * @param bounds
     */
    protected void onBoundsChange(Rect bounds) {
        initPath(bounds);
    }

    /**
     * init path
     *
     * @param bounds
     */
    protected void initPath(Rect bounds) {
        if (mSmooth) {
            initPathSmooth(bounds.left, bounds.top, bounds.right, bounds.bottom);
        } else {
            initPath(bounds.left, bounds.top, bounds.right, bounds.bottom);
        }
        invalidateSelf();
    }

    /**
     * init path
     *
     * @param left
     * @param top
     * @param right
     * @param bottom
     */
    protected void initPath(int left, int top, int right, int bottom) {
        mPath.reset();
        mPath.moveTo(left, top);

        float tagSize;
        int flu = mFluCount.left;
        if (flu > 0) {
            tagSize = (bottom - top) / (float) flu;
            for (int i = 0; i < flu; i++) {
                if (i % 2 == 0) {
                    mPath.lineTo(left, top + i * tagSize);
                } else {
                    mPath.lineTo(left + getDeepness(), top + i * tagSize);
                }
            }
        }

        mPath.lineTo(left, bottom);

        flu = mFluCount.bottom;
        if (flu > 0) {
            tagSize = (right - left) / (float) flu;
            for (int i = 0; i < flu; i++) {
                if (i % 2 == 0) {
                    mPath.lineTo(left + i * tagSize, bottom);
                } else {
                    mPath.lineTo(left + i * tagSize, bottom - getDeepness());
                }
            }
        }

        mPath.lineTo(right, bottom);

        flu = mFluCount.right;
        if (flu > 0) {
            tagSize = (bottom - top) / (float) flu;
            for (int i = 0; i < flu; i++) {
                if (i % 2 == 0) {
                    mPath.lineTo(right, bottom - (i * tagSize));
                } else {
                    mPath.lineTo(right - getDeepness(), bottom - (i * tagSize));
                }
            }
        }

        mPath.lineTo(right, top);

        flu = mFluCount.top;
        if (flu > 0) {
            tagSize = (right - left) / (float) flu;
            for (int i = 0; i < flu; i++) {
                if (i % 2 == 0) {
                    mPath.lineTo(right - (i * tagSize), top);
                } else {
                    mPath.lineTo(right - (i * tagSize), top + getDeepness());
                }
            }
        }

        mPath.lineTo(left, top);
        mPath.close();
    }

    /**
     * init path smooth
     *
     * @param left
     * @param top
     * @param right
     * @param bottom
     */
    protected void initPathSmooth(int left, int top, int right, int bottom) {
        final int maxDeep = getMaxDeepness();
        left += (mFluCount.left > 0 ? maxDeep : 0);
        top += (mFluCount.top > 0 ? maxDeep : 0);
        right -= (mFluCount.right > 0 ? maxDeep : 0);
        bottom -= (mFluCount.bottom > 0 ? maxDeep : 0);
        float x, y;
        float deep = 0, tagSize;
        mPath.reset();
        mPath.moveTo(left, top);
        x = left;
        y = top;
        int flu = mFluCount.left;
        if (flu > 0) {
            tagSize = (bottom - top) / (float) (flu * 2);
            flu--;
            for (int i = 0; i < flu; i++) {
                if (i % 2 == 0) {
                    deep = -getDeepness();
                } else {
                    deep = getDeepness();
                }
                y += tagSize;
                mPath.quadTo(x + deep, y, x, y += tagSize);
            }
            mPath.quadTo(left + deep, bottom, left, bottom);
        } else {
            mPath.lineTo(left, bottom);
        }
        y = bottom;
        flu = mFluCount.bottom;
        if (flu > 0) {
            tagSize = (right - left) / (float) (flu * 2);
            flu--;
            for (int i = 0; i < flu; i++) {
                if (i % 2 == 0) {
                    deep = getDeepness();
                } else {
                    deep = -getDeepness();
                }
                x += tagSize;
                mPath.quadTo(x, y + deep, x += tagSize, y);
            }
            mPath.quadTo(right, bottom + deep, right, bottom);
        } else {
            mPath.lineTo(right, bottom);
        }
        x = right;
        flu = mFluCount.right;
        if (flu > 0) {
            tagSize = (bottom - top) / (float) (flu * 2);
            flu--;
            for (int i = 0; i < flu; i++) {
                if (i % 2 == 0) {
                    deep = getDeepness();
                } else {
                    deep = -getDeepness();
                }
                y -= tagSize;
                mPath.quadTo(x + deep, y, x, y -= tagSize);
            }
            mPath.quadTo(right + deep, top, right, top);
        } else {
            mPath.lineTo(right, top);
        }
        y = top;
        flu = mFluCount.top;
        if (flu > 0) {
            tagSize = (right - left) / (float) (flu * 2);
            flu--;
            for (int i = 0; i < flu; i++) {
                if (i % 2 == 0) {
                    deep = -getDeepness();
                } else {
                    deep = getDeepness();
                }
                x -= tagSize;
                mPath.quadTo(x, y + deep, x -= tagSize, y);
            }
            mPath.quadTo(left, top + deep, left, top);
        } else {
            mPath.lineTo(left, top);
        }
        mPath.close();
    }

    private int getDeepness() {
        if (mRandom != null) {
            return mMinDeepness + mRandom.nextInt(mMaxDeepness - mMinDeepness);
        } else {
            return (mMaxDeepness + mMinDeepness) / 2;
        }
    }

    private int getMaxDeepness() {
        if (mRandom != null) {
            return mMaxDeepness;
        } else {
            return (mMaxDeepness + mMinDeepness) / 2;
        }
    }

    /**
     * getInterpolation
     *
     * @param input
     * @return interpolation value
     */
    public float getInterpolation(float input) {
        float result;
        if (mFactor == 1.0f) {
            result = 1.0f - (1.0f - input) * (1.0f - input);
        } else {
            result = (float) (1.0f - Math.pow(1.0f - input, 2 * mFactor));
        }
        return result;
    }

    /**
     * Use the current {@link RipCallback} implementation to have this Drawable
     * redrawn. Does nothing if there is no Callback attached to the
     * Drawable.
     *
     * @see RipCallback#invalidateDrawable
     * @see #getCallback()
     * @see #setRipCallback(RipCallback)
     */
    public void invalidateSelf() {
        final RipCallback callback = getRipCallback();
        if (callback != null) {
            callback.invalidateDrawable(this);
        }
    }

    /**
     * Use the current {@link RipCallback} implementation to have this Drawable
     * scheduled. Does nothing if there is no Callback attached to the
     * Drawable.
     *
     * @param what The action being scheduled.
     * @param when The time (in milliseconds) to run.
     * @see RipCallback#scheduleDrawable
     */
    public void scheduleSelf(Runnable what, long when) {
        final RipCallback callback = getRipCallback();
        if (callback != null) {
            callback.scheduleDrawable(this, what, when);
        }
    }

    /**
     * Use the current {@link RipCallback} implementation to have this Drawable
     * unscheduled. Does nothing if there is no Callback attached to the
     * Drawable.
     *
     * @param what The runnable that you no longer want called.
     * @see RipCallback#unscheduleDrawable
     */
    public void unscheduleSelf(Runnable what) {
        final RipCallback callback = getRipCallback();
        if (callback != null) {
            callback.unscheduleDrawable(this, what);
        }
    }

    /**
     * RipCallback
     *
     * @since 2021-05-12
     */
    public interface RipCallback {
        /**
         * Called when the drawable needs to be redrawn. A view at this point
         * should invalidate itself (or at least the part of itself where the
         * drawable appears).
         *
         * @param who The drawable that is requesting the update.
         */
        void invalidateDrawable(RipElement who);

        /**
         * A Drawable can call this to schedule the next frame of its
         * animation. An implementation can generally simply call
         * {@link ohos.eventhandler.EventHandler#postTask(Runnable, long, EventHandler.Priority)} with
         * the parameters <var>(what, who, when)</var> to perform the
         * scheduling.
         *
         * @param who The drawable being scheduled.
         * @param what The action to execute.
         * @param when The time (in milliseconds) to run. The timebase is
         * {@link ohos.miscservices.timeutility.Time#getRealActiveTime}
         */
        void scheduleDrawable(RipElement who, Runnable what, long when);

        /**
         * A Drawable can call this to unschedule an action previously
         * scheduled with {@link #scheduleDrawable}. An implementation can
         * generally simply call
         * {@link ohos.eventhandler.EventHandler#removeTask(Runnable, Object)} with
         * the parameters <var>(what, who)</var> to unschedule the drawable.
         *
         * @param who The drawable being unscheduled.
         * @param what The action being unscheduled.
         */
        void unscheduleDrawable(RipElement who, Runnable what);
    }

    public final void setRipCallback(RipCallback cb) {
        mCallback = cb != null ? new WeakReference<>(cb) : null;
    }

    public RipCallback getRipCallback() {
        return mCallback != null ? mCallback.get() : null;
    }
}
